package scanning;

public class Scanner {

	private static final char EOF = (char) -1;

	private String code;
	private int current = 0;
	private int tokenStart = 0;

	public Scanner(String code) {
		this.code = code;
	}

	public Token nextToken() {
		startToken();
		if (isDotStart())
			return scanDot();
		if (isCommaAt())
			return scanCommaAt();
		if (isComma())
			return scanComma();
		if (isVectorStart())
			return scanVectorStart();
		if (isIdentifierStart())
			return scanIdentifier();
		if (isNumberStart())
			return new NumberScanner().scan();
		if (isWhiteSpace())
			return scanWhiteSpace();
		if (isCharacterStart())
			return scanCharacter();
		if (isBooleanStart())
			return scanBoolean();
		if (is('\"'))
			return scanString();
		if (is('('))
			return scanOpeningBracket();
		if (is(')'))
			return scanClosingBracket();
		if (is('\''))
			return scanApostrophe();
		if (is('`'))
			return scanBackQuote();
		if (is(';'))
			return scanComment();
		if (is(EOF))
			return Token.EndOfFile;
		error();
		return null;
	}

	private class NumberScanner {

		private int radix;
		private String allowedDigits;
		private NumberToken numberToken = new NumberToken();
		private boolean hasOnlyImag = false;
		private int imagStart;

		public NumberToken scan() {
			setNumberBaseTen();
			scanPrefix();
			int afterPrefix = current;
			scanComplex();
			assertDelimiter();
			numberToken.setRadix(radix);
			numberToken.setText(getCurrentTokenText());
			if (!hasOnlyImag) {
				numberToken.setRealText(codePart(afterPrefix, imagStart));
				numberToken.setImagText(codePart(imagStart, current));
			} else
				numberToken.setImagText(codePart(afterPrefix, current));
			return numberToken;
		}

		private String codePart(int start, int end) {
			return code.substring(start, end).toLowerCase();
		}

		private void setNumberBaseTen() {
			radix = 10;
			allowedDigits = "0123456789";
		}

		private void scanPrefix() {
			if (isRadixStart()) {
				scanRadix();
				scanExactness();
			} else if (isExactnessStart()) {
				scanExactness();
				scanRadix();
			}
		}

		private boolean isRadixStart() {
			return is('#') && isLookAheadAnyOf("bodx");
		}

		private void scanRadix() {
			if (is('#')) {
				advance();
				scanBaseIdentifier();
				advance();
			}
		}

		private void scanBaseIdentifier() {
			if (!is('d')) {
				if (is('b'))
					setNumberBaseTwo();
				else if (is('o'))
					setNumberBaseEight();
				else
					setNumberBaseSixteen();
			}
		}

		private void setNumberBaseTwo() {
			radix = 2;
			allowedDigits = "01";
		}

		private void setNumberBaseEight() {
			radix = 8;
			allowedDigits = "01234567";
		}

		private void setNumberBaseSixteen() {
			radix = 16;
			allowedDigits = "0123456789abcdef";
		}

		private void scanExactness() {
			if (is('#')) {
				advance();
				if (is('e'))
					numberToken.setExactPrefix();
				else if (is('i'))
					numberToken.setInexactPrefix();
				else
					error();
				advance();
			}
		}

		private boolean isExactnessStart() {
			return is('#') && isLookAheadAnyOf("ie");
		}

		private void scanComplex() {
			if (isAnyOf("+-")) {
				advance();
				scanRealOrImagAfterPlusOrMinus();
			} else {
				scanReal();
				scanAtPartOrImag();
			}
		}

		private void scanRealOrImagAfterPlusOrMinus() {
			if (is('i')) {
				advance();
				hasOnlyImag = true;
			} else {
				scanUnsignedReal();
				if (is('i')) {
					advance();
					hasOnlyImag = true;
				} else
					scanAtPartOrImag();
			}
		}

		private void scanReal() {
			scanSign();
			scanUnsignedReal();
		}

		private void scanSign() {
			if (isAnyOf("+-"))
				advance();
		}

		private void scanUnsignedReal() {
			if (10 == radix)
				scanUnsignedRealWithDecimal();
			else
				scanUnsignedRealWithoutDecimal();
		}

		private void scanUnsignedRealWithDecimal() {
			if (is('.')) {
				scanDecimalStartingWithDot();
			} else {
				scanAtLeastOneDigit();
				if (is('.'))
					scanDecimalPart();
				else if (isExponentStart())
					scanExponent();
				else
					scanEndOfUnsigedRealWithDecimal();
			}
		}

		private void scanDecimalPart() {
			advance();
			while (isAnyOf(allowedDigits))
				advance();
			skipAll('#');
			scanSuffix();
		}

		private void scanSuffix() {
			if (isExponentStart())
				scanExponent();
		}

		private boolean isExponentStart() {
			return isAnyOf("esfdl");
		}

		private void scanExponent() {
			advance();
			scanSign();
			scanAtLeastOneDigit();
		}

		private void scanDecimalStartingWithDot() {
			advance();
			scanUnsignedInteger();
			scanSuffix();
		}

		private void scanUnsignedInteger() {
			scanAtLeastOneDigit();
			skipAll('#');
		}

		private void scanAtLeastOneDigit() {
			if (!isAnyOf(allowedDigits))
				error();
			advance();
			while (isAnyOf(allowedDigits))
				advance();
		}

		private void scanEndOfUnsigedRealWithDecimal() {
			skipAll('#');
			if (isExponentStart())
				scanExponent();
			else if (is('.')) {
				advance();
				skipAll('#');
				scanSuffix();
			} else if (is('/')) {
				advance();
				scanUnsignedInteger();
			}
		}

		private void scanUnsignedRealWithoutDecimal() {
			scanUnsignedInteger();
			if (is('/')) {
				advance();
				scanUnsignedInteger();
			}
		}

		private void scanAtPartOrImag() {
			imagStart = current;
			if (is('@')) {
				advance();
				scanReal();
			} else if (isAnyOf("+-")) {
				advance();
				scanImag();
			}
		}

		private void scanImag() {
			if (!is('i'))
				scanUnsignedReal();
			assertIsAndAdvance('i');
		}

	}

	private void error() throws InvalidToken {
		int length = errorLength();
		String tokenText = code.substring(tokenStart, tokenStart + length);
		throw new InvalidToken(tokenText, tokenStart, startLocation(), length);
	}

	private Location startLocation() {
		String codeTilTokenStart = code.substring(0, tokenStart);
		int lastLineBreak = codeTilTokenStart.lastIndexOf('\n');
		int column = tokenStart - lastLineBreak - 1;
		int row = numberOfNewLinesIn(codeTilTokenStart);
		return new Location(column, row);
	}

	private int numberOfNewLinesIn(String str) {
		int newLines = 0;
		for (int i = 0; i < str.length(); i++)
			if (str.charAt(i) == '\n')
				newLines++;
		return newLines;
	}

	private int errorLength() {
		int length = 0;
		int cur = tokenStart;
		while (cur < code.length() && !isDelimiter(code.charAt(cur++)))
			length++;
		return length;
	}

	@SuppressWarnings("serial")
	public static class InvalidToken extends RuntimeException {
		private String tokenText;
		private Location start;
		private int length;
		private int offset;

		public InvalidToken(String tokenText, int offset, Location start,
				int length) {
			super(tokenText + " at: " + start.toString());
			this.tokenText = tokenText;
			this.start = start;
			this.length = length;
			this.offset = offset;
		}

		public String getTokenText() {
			return tokenText;
		}

		public Location getStart() {
			return start;
		}

		public int getLength() {
			return length;
		}

		public int getOffset() {
			return offset;
		}
	}

	private void startToken() {
		tokenStart = current;
	}

	private boolean isDotStart() {
		return is('.') && isLookAheadDelimiter();
	}

	private boolean is(char c) {
		return is(c, getCurrentChar());
	}

	private char getCurrentChar() {
		if (current >= code.length())
			return EOF;
		return Character.toLowerCase(code.charAt(current));
	}

	private boolean isLookAheadDelimiter() {
		return isDelimiter(getLookAhead());
	}

	private char getLookAhead() {
		if (current + 1 < code.length())
			return Character.toLowerCase(code.charAt(current + 1));
		return EOF;
	}

	private boolean isDelimiter(char c) {
		return isWhiteSpace(c) || isAnyOf(c, "()\";") || c == EOF;
	}

	private boolean isWhiteSpace(char c) {
		return c == ' ' || c == '\t' || c == '\n';
	}

	private boolean isAnyOf(char c, String charSet) {
		for (char other : charSet.toCharArray())
			if (other == c)
				return true;
		return false;
	}

	private Token scanDot() {
		advance();
		return emit(TokenType.Dot);
	}

	private void advance() {
		current++;
	}

	private Token emit(TokenType type) {
		return new Token(type, getCurrentTokenText());
	}

	private String getCurrentTokenText() {
		return code.substring(tokenStart, current);
	}

	private boolean isCommaAt() {
		return is(',') && isLookAhead('@');
	}

	private boolean isLookAhead(char c) {
		return is(c, getLookAhead());
	}

	private Token scanCommaAt() {
		advance();
		advance();
		return emit(TokenType.CommaAt);
	}

	private boolean isComma() {
		return is(',');
	}

	private Token scanComma() {
		advance();
		return emit(TokenType.Comma);
	}

	private boolean isVectorStart() {
		return is('#') && isLookAhead('(');
	}

	private Token scanVectorStart() {
		advance();
		advance();
		return emit(TokenType.VectorStart);
	}

	private boolean isIdentifierStart() {
		return isInitial() || isSinglePlusOrMinus() || isThreeDotStart();
	}

	private boolean isInitial() {
		return isLetter() || isAnyOf("!$%&*/:<=>?~_^@");
	}

	private boolean isLetter() {
		char c = getCurrentChar();
		return c >= 'a' && c <= 'z';
	}

	private boolean isAnyOf(String charSet) {
		return isAnyOf(getCurrentChar(), charSet);
	}

	private boolean isSinglePlusOrMinus() {
		return isAnyOf("+-") && isLookAheadDelimiter();
	}

	private boolean isThreeDotStart() {
		return is('.') && isLookAhead('.');
	}

	private Token scanIdentifier() {
		if (isAnyOf("+-"))
			advance();
		else if (is('.'))
			scanThreeDots();
		else
			scanRegularIdentifier();
		return emit(TokenType.Identifier);
	}

	private void scanThreeDots() {
		advance();
		assertIsAndAdvance('.');
		assertIsAndAdvance('.');
		assertDelimiter();
	}

	private void assertDelimiter() {
		if (!isDelimiter())
			error();
	}

	private boolean isDelimiter() {
		return isDelimiter(getCurrentChar());
	}

	private boolean isSubsequent() {
		return isInitial() || isDecimalDigit() || isAnyOf("+-.");
	}

	private boolean isDecimalDigit() {
		char c = getCurrentChar();
		return c >= '0' && c <= '9';
	}

	private void scanRegularIdentifier() {
		advance();
		while (isSubsequent())
			advance();
	}

	private boolean isNumberStart() {
		return isDecimalDigit() || isAnyOf("+-.")
				|| (is('#') && isLookAheadAnyOf("bodxie"));
	}

	private boolean isLookAheadAnyOf(String charSet) {
		return isAnyOf(getLookAhead(), charSet);
	}

	private void skipAll(char c) {
		while (is(c))
			advance();
	}

	private void assertIsAndAdvance(char c) {
		if (!is(c))
			error();
		advance();
	}

	private boolean isWhiteSpace() {
		return isWhiteSpace(getCurrentChar());
	}

	private Token scanWhiteSpace() {
		advance();
		while (isWhiteSpace())
			advance();
		return emit(TokenType.WhiteSpace);
	}

	private boolean is(char toCheckFor, char actualChar) {
		return toCheckFor == actualChar;
	}

	private boolean isCharacterStart() {
		return is('#') && isLookAhead('\\');
	}

	private Token scanCharacter() {
		assertIsAndAdvance('#');
		assertIsAndAdvance('\\');
		char firstCharacter = getCurrentChar();
		advance();
		if (!isDelimiter())
			scanSpaceOrNewLineCharacter(firstCharacter);
		return emit(TokenType.Character);
	}

	private void scanSpaceOrNewLineCharacter(char character) {
		if (character == 'n')
			scanRestOfNewLineCharacter();
		else if (character == 's')
			scanRestOfSpaceCharacter();
		else
			error();
	}

	private void scanRestOfNewLineCharacter() {
		assertIsAndAdvance('e');
		assertIsAndAdvance('w');
		assertIsAndAdvance('l');
		assertIsAndAdvance('i');
		assertIsAndAdvance('n');
		assertIsAndAdvance('e');
		assertDelimiter();
	}

	private void scanRestOfSpaceCharacter() {
		assertIsAndAdvance('p');
		assertIsAndAdvance('a');
		assertIsAndAdvance('c');
		assertIsAndAdvance('e');
		assertDelimiter();
	}

	private boolean isBooleanStart() {
		return is('#') && isLookAheadAnyOf("tf");
	}

	private Token scanBoolean() {
		advance();
		advance();
		assertDelimiter();
		return emit(TokenType.Boolean);
	}

	private Token scanString() {
		advance();
		while (!is(EOF)) {
			if (is('\"'))
				break;
			if (is('\\'))
				scanSpecialCharacterInString();
			advance();
		}
		assertIsAndAdvance('\"');
		return emit(TokenType.String);
	}

	private void scanSpecialCharacterInString() {
		advance();
		if (!isAnyOf("\\\""))
			error();
	}

	private Token scanOpeningBracket() {
		advance();
		return Token.OpeningBracket;
	}

	private Token scanClosingBracket() {
		advance();
		return Token.ClosingBracket;
	}

	private Token scanApostrophe() {
		advance();
		return Token.Apostrophe;
	}

	private Token scanBackQuote() {
		advance();
		return Token.BackQuote;
	}

	private Token scanComment() {
		advance();
		while (!is(EOF) && !is('\n'))
			advance();
		return emit(TokenType.Comment);
	}

}
